﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/


using System;
using Tesla.Direct3D10.Graphics.Implementation;
using Tesla.Graphics;
using Tesla.Math;
using D3D = SlimDX.Direct3D10;

namespace Tesla.Direct3D10.Graphics {
    /// <summary>
    /// Manager that encapsulates setting render targets/backbuffers to the
    /// device.
    /// </summary>
    internal sealed class RenderTargetManager {
        private D3D10Renderer _renderer;
        private D3D.Device _graphicsDevice;
        private D3D.RenderTargetView[] _views;
        private RenderTargetBinding[] _currentRenderTargets;
        private RenderTargetBinding[] _targetsToMipMap;
        private int _targetsToMipMapCount;
        private D3D10SwapChainImplementation _activeBackBuffer;
        private int _currentRenderTargetCount;

        /// <summary>
        /// Gets the number of currently bounded render targets.
        /// </summary>
        public int CurrentRenderTargetCount {
            get {
                return _currentRenderTargetCount;
            }
        }

        /// <summary>
        /// Gets a copied array of render targets that are currently bounded.
        /// </summary>
        public RenderTargetBinding[] CurrentRenderTargets {
            get {
                RenderTargetBinding[] targets = new RenderTargetBinding[_currentRenderTargetCount];
                Array.Copy(_currentRenderTargets, targets, _currentRenderTargetCount);
                return targets;
            }
        }

        /// <summary>
        /// Creates a new render target manager.
        /// </summary>
        /// <param name="renderer">D3D10 renderer</param>
        /// <param name="maxTargets">Max targets</param>
        public RenderTargetManager(D3D10Renderer renderer, int maxTargets) {
            _renderer = renderer;
            _graphicsDevice = renderer.GraphicsDevice;
            _views = new D3D.RenderTargetView[maxTargets];
            _currentRenderTargets = new RenderTargetBinding[maxTargets];
            _targetsToMipMap = new RenderTargetBinding[maxTargets];
            _activeBackBuffer = null;
            _currentRenderTargetCount = 0;
            _targetsToMipMapCount = 0;
        }

        private void GenerateTargetMips() {
            for(int i = 0; i < _targetsToMipMapCount; i++) {
                RenderTargetBinding binding = _targetsToMipMap[i];
                //Gen cube mips
                if(binding.IsRenderTargetCube) {
                    RenderTargetCube rcube = binding.RenderTargetCube;
                    D3D10Helper.CheckDisposed(rcube);
                    if(rcube.MipCount > 1) {
                        D3D10TextureCubeImplementation impl = rcube.Implementation as D3D10TextureCubeImplementation;
                        _graphicsDevice.GenerateMips(impl.D3D10ShaderResourceView);
                    }
                //Gen 2D tex mips
                } else {
                    RenderTarget2D r2d = binding.RenderTarget2D;
                    D3D10Helper.CheckDisposed(r2d);
                    if(r2d.MipCount > 1) {
                        D3D10Texture2DImplementation impl = r2d.Implementation as D3D10Texture2DImplementation;
                        _graphicsDevice.GenerateMips(impl.D3D10ShaderResourceView);
                    }
                }
            }
        }

        /// <summary>
        /// Sets the active swap chain, typically after a swapchain.Clear() has been called.
        /// </summary>
        /// <param name="swapChainImpl">Swap chain implementation</param>
        public void SetActiveSwapChain(D3D10SwapChainImplementation swapChainImpl) {
            _activeBackBuffer = swapChainImpl;
        }

        private void SetBackBufferTargets() {
            if(_activeBackBuffer != null) {
                D3D10Helper.CheckDisposed(_activeBackBuffer);
                D3D.RenderTargetView rtv = _activeBackBuffer.D3D10RenderTargetView;
                D3D.DepthStencilView dsv = _activeBackBuffer.D3D10DepthStencilView;

                if(rtv != null) {
                    if(dsv != null) {
                        _graphicsDevice.OutputMerger.SetTargets(dsv, rtv);
                    } else {
                        _graphicsDevice.OutputMerger.SetTargets(rtv);
                    }
                }
            }
        }

        /// <summary>
        /// Clears the active targets, or back buffer if there are none.
        /// </summary>
        /// <param name="options"></param>
        /// <param name="color"></param>
        /// <param name="depth"></param>
        /// <param name="stencil"></param>
        public void ClearTargets(ClearOptions options, Color color, float depth, int stencil) {
            D3D10Helper.CheckDisposed(_graphicsDevice);
            SlimDX.Color4 sc;
            D3D10Helper.Convert(ref color, out sc);
            if(_currentRenderTargetCount == 0 && _activeBackBuffer != null) {
                D3D10Helper.CheckDisposed(_activeBackBuffer);
                if(_activeBackBuffer.D3D10DepthStencilView != null) {
                    if(((options & ClearOptions.Depth) == ClearOptions.Depth) &&((options & ClearOptions.Stencil) == ClearOptions.Stencil)) {
                        _graphicsDevice.ClearDepthStencilView(_activeBackBuffer.D3D10DepthStencilView, D3D.DepthStencilClearFlags.Depth | D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                    } else if((options & ClearOptions.Depth) == ClearOptions.Depth) {
                        _graphicsDevice.ClearDepthStencilView(_activeBackBuffer.D3D10DepthStencilView, D3D.DepthStencilClearFlags.Depth, depth, (byte) stencil);
                    } else if((options & ClearOptions.Stencil) == ClearOptions.Stencil) {
                        _graphicsDevice.ClearDepthStencilView(_activeBackBuffer.D3D10DepthStencilView, D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                    }
                }
                if((_activeBackBuffer.D3D10RenderTargetView != null) && ((options & ClearOptions.Target) == ClearOptions.Target)) {
                    _graphicsDevice.ClearRenderTargetView(_activeBackBuffer.D3D10RenderTargetView, sc);
                }
            } else {
                for(int i = 0; i < _currentRenderTargetCount; i++) {
                    RenderTargetBinding binding = _currentRenderTargets[i];
                    D3D.DepthStencilView dsv = null;
                    D3D.RenderTargetView rtv = null;

                    if(!binding.IsRenderTargetCube) {
                        D3D10Texture2DImplementation impl2D = binding.RenderTarget2D.Implementation as D3D10Texture2DImplementation;
                        D3D10Helper.CheckDisposed(impl2D);
                        dsv = impl2D.D3D10DepthStencilView;
                        rtv = impl2D.D3D10RenderTargetVew;
                    } else {
                        D3D10TextureCubeImplementation implcube = binding.RenderTargetCube.Implementation as D3D10TextureCubeImplementation;
                        D3D10Helper.CheckDisposed(implcube);
                        dsv = implcube.D3D10DepthStencilView;
                        rtv = implcube.GetRenderTargetView(binding.CubeMapFace);
                    }

                    if(dsv != null) {
                        if(((options & ClearOptions.Depth) == ClearOptions.Depth) &&((options & ClearOptions.Stencil) == ClearOptions.Stencil)) {
                            _graphicsDevice.ClearDepthStencilView(dsv, D3D.DepthStencilClearFlags.Depth | D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                        } else if((options & ClearOptions.Depth) == ClearOptions.Depth) {
                            _graphicsDevice.ClearDepthStencilView(dsv, D3D.DepthStencilClearFlags.Depth, depth, (byte) stencil);
                        } else if((options & ClearOptions.Stencil) == ClearOptions.Stencil) {
                            _graphicsDevice.ClearDepthStencilView(dsv, D3D.DepthStencilClearFlags.Stencil, depth, (byte) stencil);
                        }
                    }

                    if((rtv != null) && ((options & ClearOptions.Target) == ClearOptions.Target)) {
                        _graphicsDevice.ClearRenderTargetView(rtv, sc);
                    }
                }
            }
        }

        public void SetRenderTargets(RenderTargetBinding[] targets) {
            D3D10Helper.CheckDisposed(_graphicsDevice);

            if(targets == null || targets.Length == 0) {
                //Clear targets
                ClearTargets();

                //Set the swapchain's targets to the device, unbinding the current ones
                SetBackBufferTargets();

                //Generate mip maps of previously bounded targets
                GenerateTargetMips();
            } else {
                //If we have a larger array than the max targets possible, throw an exception
                if(targets.Length > _currentRenderTargets.Length) {
                    throw new ArgumentOutOfRangeException("Cannot bind more than 8 targets at a time.");
                }

                //Clear targets
                ClearTargets();

                D3D.RenderTargetView rtv;
                D3D.DepthStencilView dsv = null;
                DepthFormat depthFormat = DepthFormat.None;
                int width = 0;
                int height = 0;
                int samples = 0;
                int pixelDepth = 0;
                for(int i = 0; i < targets.Length; i++) {
                    RenderTargetBinding binding = targets[i];
                    RenderTargetUsage usage = RenderTargetUsage.DiscardContents;
                    if(binding.IsRenderTargetCube) {
                        D3D10TextureCubeImplementation cubeImpl = binding.RenderTargetCube.Implementation as D3D10TextureCubeImplementation;
                        D3D10Helper.CheckDisposed(cubeImpl);
                        rtv = cubeImpl.GetRenderTargetView(binding.CubeMapFace);
                        usage = cubeImpl.TargetUsage;
                        if(i == 0) {
                            width = cubeImpl.Size;
                            height = cubeImpl.Size;
                            dsv = cubeImpl.D3D10DepthStencilView;
                            depthFormat = cubeImpl.DepthFormat;
                            samples = cubeImpl.MultiSampleCount;
                            pixelDepth = D3D10Helper.FormatSize(cubeImpl.Format);
                        } else {
                            if(cubeImpl.Size != width || cubeImpl.Size != height || cubeImpl.MultiSampleCount != samples 
                                || D3D10Helper.FormatSize(cubeImpl.Format) != pixelDepth) {
                                throw new ArgumentException("All render target dimensions must match.");
                            }
                        }
                    } else {
                        D3D10Texture2DImplementation texImpl = binding.RenderTarget2D.Implementation as D3D10Texture2DImplementation;
                        D3D10Helper.CheckDisposed(texImpl);
                        rtv = texImpl.D3D10RenderTargetVew;
                        usage = texImpl.TargetUsage;
                        if(i == 0) {
                            width = texImpl.Width;
                            height = texImpl.Height;
                            dsv = texImpl.D3D10DepthStencilView;
                            depthFormat = texImpl.DepthFormat;
                            samples = texImpl.MultiSampleCount;
                            pixelDepth = D3D10Helper.FormatSize(texImpl.Format);
                        } else {
                            if(texImpl.Width != width || texImpl.Height != height || texImpl.MultiSampleCount != samples 
                                || D3D10Helper.FormatSize(texImpl.Format) != pixelDepth) {
                                throw new ArgumentException("All render target dimensions, sample count, and bit depth must match.");
                            }
                        }
                    }

                    //Set the targets to the arrays, after we're done processing we set them all at once
                    _currentRenderTargets[i] = binding;
                    _views[i] = rtv;
                    _currentRenderTargetCount++;

                    //Check usage, if we're using a target that isn't preserve contents,
                    //then we clear the target.
                    if(usage != RenderTargetUsage.PreserveContents) {
                        _graphicsDevice.ClearRenderTargetView(rtv, new SlimDX.Color4(1.0f, 0, 0, 0));
                        if(i == 0 && dsv != null) {
                            D3D.DepthStencilClearFlags flags = D3D.DepthStencilClearFlags.Depth;
                            if(depthFormat == DepthFormat.Depth24Stencil8) {
                                flags |= D3D.DepthStencilClearFlags.Stencil;
                            }
                            _graphicsDevice.ClearDepthStencilView(dsv, flags, 1.0f, 0);
                        }
                    }
                }

                //Bind targets
                if(dsv != null) {
                    _graphicsDevice.OutputMerger.SetTargets(dsv, _views);
                } else {
                    _graphicsDevice.OutputMerger.SetTargets(_views);
                }

                //Generate mip maps of previously bounded targets
                GenerateTargetMips();

                //Note: What if a user binds a target here that was previously bound,
                //is generating mip maps on a bound target ok? I don't know if
                //a user would even want to do that...
            }
        }

        private void ClearTargets() {
            //Copy targets
            Array.Copy(_currentRenderTargets, _targetsToMipMap, _currentRenderTargetCount);
            _targetsToMipMapCount = _currentRenderTargetCount;

            //Clear the current targets
            Array.Clear(_currentRenderTargets, 0, _currentRenderTargetCount);
            Array.Clear(_views, 0, _currentRenderTargetCount);
            _currentRenderTargetCount = 0;
        }

        public void ClearAll() {
            Array.Clear(_currentRenderTargets, 0, _currentRenderTargets.Length);
            Array.Clear(_targetsToMipMap, 0, _targetsToMipMap.Length);
            Array.Clear(_views, 0, _views.Length);
        }
    }
}
